﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using ParserGenerator.Collections;

namespace ParserGenerator.Extensions
{
    public static class StringExtensions
    {
        public static ReadOnlySet<char> BashSafeChars { get; }

        static StringExtensions()
        {
            HashSet<char> chars = new HashSet<char>();
            for (char c = 'A'; c <= 'Z'; c++)
                chars.Add(c);
            for (char c = 'a'; c <= 'z'; c++)
                chars.Add(c);
            for (char c = '0'; c <= '9'; c++)
                chars.Add(c);
            chars.UnionWith("+-_=/%@:,.");
            BashSafeChars = chars.AsReadOnly();
        }

        public static bool IsNullOrEmpty(this string value)
        {
            return string.IsNullOrEmpty(value);
        }

        public static bool IsNullOrWhiteSpace(this string value)
        {
            return string.IsNullOrWhiteSpace(value);
        }

        public static IEnumerable<int> EnumCodepoints(this string value)
        {
            if (value == null)
                throw new ArgumentNullException(nameof(value));
            for (int i = 0; i < value.Length; i++)
                if (char.IsSurrogatePair(value, i))
                    yield return char.ConvertToUtf32(value, i++);
                else
                    yield return value[i];
        }

        public static int[] GetCodepoints(this string value)
        {
            if (value == null)
                throw new ArgumentNullException(nameof(value));
            return EnumCodepoints(value).ToArray();
        }

        public static string Format(this string value, char delimiter)
        {
            if (value == null)
                throw new ArgumentNullException(nameof(value));
            if (!delimiter.IsQuoteDelimiter())
                throw new ArgumentException(SR.DelimiterMustBeQuote, nameof(delimiter));
            StringBuilder sb = new StringBuilder();
            sb.Append(delimiter);
            foreach (int c in value.EnumCodepoints())
                sb.Append(c.Escape(delimiter));
            sb.Append(delimiter);
            return sb.ToString();
        }

        public static string AsSingleQuote(this string value)
        {
            if (value == null)
                throw new ArgumentNullException(nameof(value));
            return value.Format('\'');
        }

        public static string AsDoubleQuote(this string value)
        {
            if (value == null)
                throw new ArgumentNullException(nameof(value));
            return value.Format('"');
        }

        public static string AsBackQuote(this string value)
        {
            if (value == null)
                throw new ArgumentNullException(nameof(value));
            return value.Format('`');
        }

        public static string AsHtmlText(this string value)
        {
            if (value == null)
                throw new ArgumentNullException(nameof(value));
            StringBuilder sb = new StringBuilder();
            foreach (int c in value.EnumCodepoints())
            {
                switch (c)
                {
                    case '\'':
                        sb.Append("&apos;");
                        break;
                    case '"':
                        sb.Append("&quot;");
                        break;
                    case '<':
                        sb.Append("&lt;");
                        break;
                    case '>':
                        sb.Append("&gt;");
                        break;
                    case '&':
                        sb.Append("&amp;");
                        break;
                    case ' ':
                        sb.Append("&nbsp;");
                        break;
                    default:
                        if (' ' <= c && c <= 0x7E)
                            sb.Append((char)c);
                        else
                            sb.AppendFormat("&#x{0:X};", c);
                        break;
                }
            }
            return sb.ToString();
        }

        public static string AsUnixProgramName(this string value)
        {
            if (value == null)
                throw new ArgumentNullException(nameof(value));
            return value.EscapeBash();
        }

        public static string AsWindowsProgramName(this string value)
        {
            if (value == null)
                throw new ArgumentNullException(nameof(value));
            bool needQuote = value.Length == 0 || value.IndexOfAny(new[] { ' ', '\t' }) != -1;
            if (!needQuote)
                return value;
            return '"' + value + '"';
        }

        public static string AsProgramName(this string value)
        {
            if (value == null)
                throw new ArgumentNullException(nameof(value));
            PlatformID id = Environment.OSVersion.Platform;
            if (id == PlatformID.Unix || id == PlatformID.MacOSX)
                return value.AsUnixProgramName();
            else
                return value.AsWindowsProgramName();
        }

        public static string AsUnixProgramArgument(this string value)
        {
            if (value == null)
                throw new ArgumentNullException(nameof(value));
            return value.EscapeBash();
        }

        public static string AsWindowsProgramArgument(this string value)
        {
            if (value == null)
                throw new ArgumentNullException(nameof(value));
            StringBuilder sb = new StringBuilder();
            StringBuilder buf = new StringBuilder();
            bool needQuote = value.Length == 0 || value.IndexOfAny(new[] { ' ', '\t' }) != -1;
            if (needQuote)
                sb.Append('"');
            foreach (char c in value)
            {
                if (c == '\\')
                    buf.Append('\\');
                else if (c == '"')
                {
                    sb.Append('\\', buf.Length * 2);
                    sb.Append("\\\"");
                    buf.Clear();
                }
                else
                {
                    if (buf.Length > 0)
                    {
                        sb.Append(buf);
                        buf.Clear();
                    }
                    sb.Append(c);
                }
            }
            if (buf.Length > 0)
                sb.Append(buf);
            if (needQuote)
            {
                sb.Append(buf);
                sb.Append('"');
            }
            return sb.ToString();
        }

        public static string AsProgramArgument(this string value)
        {
            if (value == null)
                throw new ArgumentNullException(nameof(value));
            PlatformID id = Environment.OSVersion.Platform;
            if (id == PlatformID.Unix || id == PlatformID.MacOSX)
                return value.AsUnixProgramArgument();
            else
                return value.AsWindowsProgramArgument();
        }

        public static string JoinAsUnixProgramArguments(this IEnumerable<string> value)
        {
            if (value == null)
                throw new ArgumentNullException(nameof(value));
            return string.Join(" ", from x in value select x.AsUnixProgramArgument());
        }

        public static string JoinAsWindowsProgramArguments(this IEnumerable<string> value)
        {
            if (value == null)
                throw new ArgumentNullException(nameof(value));
            return string.Join(" ", from x in value select x.AsWindowsProgramArgument());
        }

        public static string JoinAsProgramArguments(this IEnumerable<string> value)
        {
            if (value == null)
                throw new ArgumentNullException(nameof(value));
            PlatformID id = Environment.OSVersion.Platform;
            if (id == PlatformID.Unix || id == PlatformID.MacOSX)
                return value.JoinAsUnixProgramArguments();
            else
                return value.JoinAsWindowsProgramArguments();
        }

        public static string BuildCommandLine(this string program, params string[] args)
        {
            if (program == null)
                throw new ArgumentNullException(nameof(program));
            if (args == null)
                throw new ArgumentNullException(nameof(args));
            string command = program.AsProgramName();
            if (args.Length == 0)
                return command;
            string arguments = args.JoinAsProgramArguments();
            return command + " " + arguments;
        }

        public static string EscapeBash(this string value)
        {
            if (value == null)
                throw new ArgumentNullException(nameof(value));
            bool needEscape = value.Any(c => !BashSafeChars.Contains(c));
            if (!needEscape)
                return value;
            StringBuilder sb = new StringBuilder();
            foreach (char c in value)
            {
                if (!BashSafeChars.Contains(c))
                    sb.Append('\\');
                sb.Append(c);
            }
            return sb.ToString();
        }

        public static IEnumerable<string> EnumLines(this string value)
        {
            if (value == null)
                throw new ArgumentNullException(nameof(value));
            int j = 0;
            while (j < value.Length)
            {
                int i = j;
                while (j < value.Length && !value[j].IsLineTerminator())
                    j++;
                yield return value.Substring(i, j - i);
                if (j < value.Length)
                    if (value[j] == '\r' && j + 1 < value.Length && value[j + 1] == '\n')
                        j += 2;
                    else
                        j++;
            }
        }

        public static string TryGetLine(this string value, int line)
        {
            if (value == null)
                throw new ArgumentNullException(nameof(value));
            if (line < 0)
                throw new ArgumentOutOfRangeException(nameof(line), SR.LineMustBeNonNegative);
            int index = 0;
            int j = 0;
            while (j < value.Length)
            {
                int i = j;
                while (j < value.Length && !value[j].IsLineTerminator())
                    j++;
                if (index++ == line)
                    return value.Substring(i, j - i);
                if (j < value.Length)
                    if (value[j] == '\r' && j + 1 < value.Length && value[j + 1] == '\n')
                        j += 2;
                    else
                        j++;
            }
            return null;
        }

        public static string GetLine(this string value, int line)
        {
            if (value == null)
                throw new ArgumentNullException(nameof(value));
            if (line < 0)
                throw new ArgumentOutOfRangeException(nameof(line), SR.LineMustBeNonNegative);
            string str = TryGetLine(value, line);
            if (str == null)
                throw new ArgumentOutOfRangeException(nameof(line), SR.LineOutOfRange);
            return str;
        }

        public static string TryGetLineAtCharIndex(this string value, int index)
        {
            if (value == null)
                throw new ArgumentNullException(nameof(value));
            if (index < 0)
                throw new ArgumentOutOfRangeException(nameof(index), SR.IndexMustBeNonNegative);
            if (index > value.Length)
                return null;
            int i = index;
            int j = index;
            while (i > 0 && !value[i - 1].IsLineTerminator())
                i--;
            while (j < value.Length && !value[j].IsLineTerminator())
                j++;
            return value.Substring(i, j - i);
        }

        public static string GetLineAtCharIndex(this string value, int index)
        {
            if (value == null)
                throw new ArgumentNullException(nameof(value));
            if (index < 0)
                throw new ArgumentOutOfRangeException(nameof(index), SR.IndexMustBeNonNegative);
            string str = TryGetLineAtCharIndex(value, index);
            if (str == null)
                throw new ArgumentOutOfRangeException(nameof(index), SR.IndexOutOfRange);
            return str;
        }

        public static string ToPascal(this string name)
        {
            if (name == null)
                throw new ArgumentNullException(nameof(name));
            if (name.Length > 0 && char.IsLower(name[0]))
                return char.ToUpper(name[0]) + name.Substring(1);
            else
                return name;
        }
    }
}
